﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace RayDen.Library.Components.PhotonMapping
{
    using System.Runtime.InteropServices;


    public class HashGridPhotonMap : IPhotonMap
    {
        public static float NormalThreshold = 0f;
        public static float Radius = 0.0005f;
        public static float CellSizeScale = 0.21f;

        //2f / 3f;

        public static int MaxPhotonsPerCell = 16;
        public List<Photon> hitPoints;

        public Photon[][] hashGrid;

        private uint hashGridSize;
        private int photonsCount;
        private int maxPhotons;
        private float invCellSize;
        public AABB hpbbox;

        public ulong PhotonsCount
        {
            get { return (ulong)this.photonsCount; }
        }


        public HashGridPhotonMap(int max)
        {
            maxPhotons = max;
            this.hitPoints = Enumerable.Repeat<Photon>(new Photon(), max).ToList();
            //new List<Photon>(max);

            this.photonsCount = 0;
        }


        public void QueryMap(ref Point pos, float maxDist, int nphotons, out Photon[] result)
        {

            result = new Photon[nphotons];
            var ix = this.hashGrid[(int)this.Hash(ref pos)];

            result = ix.ToArray();
        }

        public void Store(ref RgbSpectrum power, ref Point position, ref Vector dir)
        {
            if (this.photonsCount > (this.maxPhotons - 2))
                return;



            if (power.Length <= 0f)
                return;
            var dirx = MathLab.Clamp(dir.z, -1f, 1f);
            int theta = Convert.ToInt32(Math.Acos(dirx) * (256.0d / Math.PI));
            if (theta > 255)
                theta = 255;
            var thetaB = (byte)theta;
            int phi = Convert.ToInt32(Math.Atan2(dir.y, dir.x) * (256.0d / 2.0 * Math.PI));
            if (phi > 255)
                phi = 255;
            else if (phi < 0)
                phi = phi + 256;
            var phiB = (byte)phi;
            this.hitPoints[photonsCount] = new Photon()
            {
                Position = position,
                Power = power,
                Theta = thetaB,
                Phi = phiB
            };
            this.photonsCount++;
        }



        public void ReHash()
        {
            if (this.hitPoints.Count == 0)
                return;
            Tracer.TraceLine("Rehashing...");
            var hitPointsCount = this.hitPoints.Count;
            this.hpbbox = new AABB();
            this.hpbbox.Reset();
            for (int n = 0; n < this.hitPoints.Count; n++)
            {
                this.hpbbox.fit(this.hitPoints[n].Position);

            }
            Vector ssize = this.hpbbox.Size;
            float photonRadius1 = Radius;
            //((ssize.x + ssize.y + ssize.z) / 3.0f) / (ssize.Length2());

            // Expand the bounding box by used radius
            this.hpbbox.Expand(photonRadius1);

            // Initialize hit points field
            float photonRadius2 = photonRadius1 * photonRadius1;

            // Calculate the size of the grid cell
            float maxPhotonRadius2 = 0f;
            for (int i = 0; i < hitPointsCount; ++i)
            {
                var hp = this.hitPoints[i];

                //if (hp.type == HitPointType.SURFACE)
                maxPhotonRadius2 = Math.Max(maxPhotonRadius2, photonRadius2);
            }

            float cellSize = MathLab.Sqrt(maxPhotonRadius2) * CellSizeScale;
            //std::cerr << "Hash grid cell size: " << cellSize <<std::endl;
            this.invCellSize = 1f / cellSize;
            if (float.IsInfinity(this.invCellSize))
                throw new ArgumentException("Cell size is invalid");

            // TODO: add a tunable parameter for hashgrid size
            this.hashGridSize = (uint)hitPointsCount;


            var hg = new List<List<Photon>>(1000);

            //for (var i = 0; i < this.hashGridSize; ++i)this.hashGrid.Add(null);

            //std::cerr << "Building hit points hash grid:" << std::endl;
            //std::cerr << "  0k/" << hitPointsCount / 1000 << "k" <<std::endl;
            //unsigned int maxPathCount = 0;
            //double lastPrintTime = WallClockTime();
            ulong entryCount = 0;
            var hppMin = (Vector)this.hpbbox.Min;

            for (var i = 0; i < hitPointsCount; ++i)
            {
                //if (WallClockTime() - lastPrintTime > 2.0) {
                //    std::cerr << "  " << i / 1000 << "k/" << hitPointsCount / 1000 << "k" <<std::endl;
                //    lastPrintTime = WallClockTime();
                //}

                var hp = this.hitPoints[i];
                this.hpbbox.fit(this.hitPoints[i].Position - photonRadius1);
                this.hpbbox.fit(this.hitPoints[i].Position + photonRadius1);
                {
                    float photonRadius = MathLab.Sqrt(maxPhotonRadius2);
                    Vector rad = new Vector(photonRadius, photonRadius, photonRadius);
                    Vector bMin = ((hp.Position - rad) - hppMin) * this.invCellSize;
                    Vector bMax = ((hp.Position + rad) - hppMin) * this.invCellSize;


                    var hgl = new List<Photon>(MaxPhotonsPerCell);
                    hgl.Add(hp);
                    for (int iz = Math.Abs((int)bMin.z); iz <= Math.Abs((int)bMax.z); iz++)
                    {
                        for (int iy = Math.Abs((int)bMin.y); iy <= Math.Abs((int)bMax.y); iy++)
                        {
                            for (int ix = Math.Abs((int)bMin.x); ix <= Math.Abs((int)bMax.x); ix++)
                            {
                                int hv = (int)this.Hash(ix, iy, iz);

                                if (hg.Count < hv)
                                {
                                    hg.AddRange(Enumerable.Repeat(hgl, hv + 1));
                                }

                                if (hg[hv] == null)
                                    hg[hv] = hgl;

                                //new List<int>(MaxPhotonsPerCell);
                                if (hg[hv].Count == 0)
                                {
                                    hg[hv].Add(hp);
                                }


                                if (hg[hv].Count > MaxPhotonsPerCell)
                                {
                                    MaxPhotonsPerCell = hg[hv].Count;
                                }


                                ++entryCount;

                                /*// hashGrid[hv].size() is very slow to execute
                                if (hashGrid[hv].size() > maxPathCount)
                                    maxPathCount = hashGrid[hv].size();*/
                            }
                        }
                    }
                }
            }
            //std::cerr << "Max. hit points in a single hash grid entry: " << maxPathCount << std::endl;
            //std::cerr << "Total hash grid entry: " << entryCount << std::endl;
            //std::cerr << "Avg. hit points in a single hash grid entry: " << entryCount / hashGridSize << std::endl;

            // HashGrid debug code
            /*for (unsigned int i = 0; i < hashGridSize; ++i) {
                if (hashGrid[i]) {
                    if (hashGrid[i].size() > 10) {
                        std::cerr << "HashGrid[" << i << "].size() = " <<hashGrid[i].size() << std::endl;
                    }
                }
            }*/
            //var hgc = hg.Where(item=>item != null).Sum(item => item.Count);
            var hgc = PhotonsCount;
            Tracer.TraceLine("Total Photons {0} Item Size {1} Total Size {2:F1} KBytes ",
                              hitPoints.Count,
                              Marshal.SizeOf(typeof(Photon)),
                              hitPoints.Count * Marshal.SizeOf(typeof(Photon)) / 1000f);
            Tracer.TraceLine("Total Items in Grid {0} Item Size {1} Total Size {2:F1} KBytes ", hgc, 4, (hgc * 4f) / 1000f);
            this.hashGrid = hg.Select(item => item.ToArray()).ToArray();
            GC.Collect();
            Tracer.TraceLine("Complete...");
        }

        private uint Hash(int ix, int iy, int iz)
        {
            return ((uint)((ix * 73856093U) ^ (iy * 19349663U) ^ (iz * 83492791U)) % this.hashGridSize);
        }

        private uint Hash(ref Vector i)
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }

        private uint Hash(ref Point i)
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }

        private uint Hash(Vector i)
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }

        private uint Hash(Point i)
        {
            return ((((uint)i.x * 73856093U) ^ ((uint)i.y * 19349663U) ^ ((uint)i.z * 83492791U)) % this.hashGridSize);
        }

        public static Vector PhotonDirection(Photon p)
        {
            return new Vector(
                DirectionDiscretizationHelper.SinTheta(p.Theta) * DirectionDiscretizationHelper.CosPhi(p.Phi),
                DirectionDiscretizationHelper.SinTheta(p.Theta) * DirectionDiscretizationHelper.SinPhi(p.Phi),
                DirectionDiscretizationHelper.CosTheta(p.Theta)
                );
        }

        public Vector IrradianceEstimate(ref Point pos, ref Normal n, float max_dist, int nphotons)
        {

            var photons = this.hashGrid[(int)this.Hash(ref pos)].ToList();
            var irrad = new Vector();
            if (photons == null)
            {
                return irrad;
            }
            if (photons.Count < nphotons)
            {
                var minp = this.hashGrid[(int)this.Hash(pos - 1f / invCellSize)];
                var maxp = this.hashGrid[(int)this.Hash(pos + 1f / invCellSize)];
                if (minp != null)
                    photons.AddRange(minp);
                if (maxp != null)
                    photons.AddRange(maxp);
            }

            for (int index = 0; index < Math.Min(photons.Count, nphotons); index++)
            {
                Photon photon = photons[index];
                Vector pdir =
                    //photon.Direction;
                    PhotonDirection(photon);
                var dot = Vector.Dot(ref pdir, ref n);
                //var dist = (photon.Position - pos).Length;
                if (dot > NormalThreshold)
                {
                    irrad += (Vector)photon.Power; //*BRDF
                }
            }
            var tmp = 1.0f / (MathLab.M_PI * max_dist * max_dist);
            irrad = irrad * tmp;
            return irrad;
        }


        #region IPhotonMap Members

        public Photon[] GetPlainData()
        {
            return this.hitPoints.ToArray();
        }

        public void ScalePhotonMap(float scale)
        {

        }

        public void SavePhotonMap(string fileName)
        {

        }

        public void LoadPhotonMap(string fileName)
        {

        }

        private bool hashed = false;

        public void Balance()
        {
            try
            {

                if (hashed)
                    return;
                this.ReHash();
                hashed = true;
                return;
                /*
                if (!this.hashed) {
                    this.ReHash();
                    this.hashed = true;
                }
                 */
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.Message);
                Tracer.TraceLine(ex.StackTrace);
            }
        }

        #endregion
    }
}                           